"""
Author: Morphlng
Date: 2024-01-23 13:33:46
LastEditTime: 2025-04-21 09:16:54
LastEditors: Morphlng
Description: Adapter for CARLA simulator
FilePath: /DrivingGym/src/driving_gym/simulation/adapter/carla/carla_adapter.py
"""

from __future__ import annotations

import importlib
import logging
import random
from typing import Callable

import carla

import driving_gym.simulation.adapter.carla.utils as carla_util
from driving_gym.misc.util import override, retry
from driving_gym.simulation.adapter.carla.carla_birdeye_view import (
    BirdViewCropType,
    BirdViewProducer,
)
from driving_gym.simulation.adapter.carla.carla_config import (
    CarlaActorConfig,
    CarlaSensorConfig,
)
from driving_gym.simulation.adapter.carla.carla_sensor import (
    CarlaSensor,
    PseudoSensor,
    wrap_sensor,
)
from driving_gym.simulation.adapter.carla.carla_traffic import (
    generate_vehicle_traffic,
    generate_walker_traffic,
)
from driving_gym.simulation.adapter.carla.replay_parser import Replay
from driving_gym.simulation.adapter_interface import AdapterInterface
from driving_gym.simulation.common.hd_map import HDMap

logger = logging.getLogger(__name__)


class CarlaAdapter(AdapterInterface):
    """Simulator Adapter for CARLA."""

    def __init__(self, config: dict):
        super().__init__(config)
        self._cnt = 0
        self._first_load = True
        self._is_replaying = False
        self._process = None
        self._client: carla.Client = None
        self._world: carla.World = None
        self._map: carla.Map = None
        self._spawn_points: list[carla.Transform] = None
        self._blueprint_library: carla.BlueprintLibrary = None
        self._traffic_manager: carla.TrafficManager = None
        self._traffic_vehicles: dict[str, carla.Vehicle] = {}
        self._traffic_walkers: dict[str, "carla.Walker | carla.WalkerAIController"] = {}
        self._hd_map: HDMap = None
        self._actor_pool: dict[str, carla.Actor] = {}
        self._autopilot_pool: dict[str, bool] = {}
        self._sensor_pool: dict[str, CarlaSensor] = {}
        self._global_callbacks: list[int] = []
        self._command_buffer: list = []
        self.parse_config()

    @property
    def client(self) -> carla.Client:
        if self._client is None:
            raise RuntimeError("Carla client is not initialized.")
        return self._client

    @property
    def blueprint_library(self) -> carla.BlueprintLibrary:
        if self._blueprint_library is None:
            self._blueprint_library = self.world.get_blueprint_library()
        return self._blueprint_library

    @property
    def world(self) -> carla.World:
        if self._world is None:
            self._world = self.client.get_world()
        return self._world

    @property
    def map(self) -> carla.Map:
        if self._map is None:
            try:
                self._map = self.world.get_map()
            except RuntimeError as e:
                if "OpenDRIVE" in str(e):
                    logger.warning("The map you're using does not have XODR file.")
        return self._map

    @property
    def spawn_points(self) -> list[carla.Transform]:
        if self._spawn_points is None:
            self._spawn_points = self.map.get_spawn_points()
        return self._spawn_points

    @property
    def hd_map(self) -> HDMap:
        if self._hd_map is None:
            self._hd_map = HDMap(self.map)
        return self._hd_map

    @property
    def traffic_manager(self) -> carla.TrafficManager:
        if self._traffic_manager is None:
            raise RuntimeError("You must initialize carla client first.")
        return self._traffic_manager

    @override(AdapterInterface)
    def initialize(self):
        """Establish connection to the simulator."""

        if self.start_server:
            self._process = carla_util.start_local_server(
                port=self.port, offscreen=self.offsreen
            )

        self._client = self.connect(self.host, self.port)
        self._client.set_timeout(self.timeout)

        self._traffic_manager = self.client.get_trafficmanager(self.tm_port)
        self._traffic_manager.set_hybrid_physics_mode(self.hybrid_physics_mode)
        self._traffic_manager.set_hybrid_physics_radius(self.hybrid_physics_radius)
        if self.random_seed:
            self._traffic_manager.set_random_device_seed(self.random_seed)

    @override(AdapterInterface)
    def cleanup(self):
        """Clean up simulator resources"""

        if self._client is None:
            return

        for callback in self._global_callbacks:
            self.world.remove_on_tick(callback)

        self.clear_traffic()

        for sensor in self._sensor_pool.values():
            sensor.destroy()
            self.tick()

        actor_batch = []
        for actor in list(self._actor_pool.values()):
            actor_batch.append(carla.command.DestroyActor(actor))
        self.client.apply_batch_sync(actor_batch)
        self.tick()

        self.toggle_sync_mode(False, None)
        self.reset()

        self._client = None
        self._traffic_manager = None
        if self._process is not None:
            carla_util.stop_local_server(self._process)
            self._process = None

    @override(AdapterInterface)
    def get_map(self):
        return self.hd_map

    @override(AdapterInterface)
    def get_transform(self, identifier: str):
        actor = self.get_actor(identifier)
        return carla_util.localize_transform(actor.get_transform())

    @override(AdapterInterface)
    def get_velocity(self, identifier: str):
        actor = self.get_actor(identifier)
        return carla_util.localize_vector(actor.get_velocity())

    @override(AdapterInterface)
    def get_acceleration(self, identifier: str):
        actor = self.get_actor(identifier)
        return carla_util.localize_vector(actor.get_acceleration())

    @override(AdapterInterface)
    def get_bounding_box(self, identifier: str):
        actor = self.get_actor(identifier)
        return carla_util.localize_bounding_box(actor.bounding_box)

    @override(AdapterInterface)
    def get_sensor_data(self, identifier: str):
        sensor = self.get_sensor(identifier)
        return sensor.get_data()

    @override(AdapterInterface)
    def get_actors(self):
        traffic_vehicles = list(self._traffic_vehicles.keys())
        traffic_walkers = list(self._traffic_walkers.keys())[1::2]
        actors = list(self._actor_pool.keys()) + traffic_vehicles + traffic_walkers
        return actors

    @override(AdapterInterface)
    def get_sensors(self):
        return list(self._sensor_pool.keys())

    @override(AdapterInterface)
    def get_control(self, identifier: str):
        actor = self.get_actor(identifier)
        return actor.get_control()

    @override(AdapterInterface)
    def register_callback(self, func: Callable):
        self._global_callbacks.append(self.world.on_tick(func))

    @override(AdapterInterface)
    def apply_control(self, target: str, control: dict):
        if control is None or self._autopilot_pool.get(target, False):
            return

        actor = self.get_actor(target)
        control_type = control.get("type", "vehicle").lower()

        if control_type == "vehicle":
            carla_control = carla_util.vehicle_control(control)
            if self.command_in_batch:
                self._command_buffer.append(
                    carla.command.ApplyVehicleControl(actor, carla_control)
                )
            else:
                actor.apply_control(carla_control)
        elif control_type == "walker":
            carla_control = carla_util.walker_control(control)
            if self.command_in_batch:
                self._command_buffer.append(
                    carla.command.ApplyWalkerControl(actor, carla_control)
                )
            else:
                actor.apply_control(carla_control)
        else:
            applier = getattr(carla_util, f"{control_type}_control", None)
            if applier is None:
                raise ValueError(f"Unsupported control type: {control_type}")
            else:
                applier(actor, control)

    @override(AdapterInterface)
    def load_scenario(self, scenario: dict):
        self._cnt += 1
        self._is_replaying = False
        replay_file: str = scenario.get("replay_file", None)
        replay_config: dict = scenario.get("replay_config", {})
        map_name: str = scenario["map"]
        actors: dict = scenario["actor"]
        tasks: dict = scenario["task"]

        if self.reload_epochs > 0 and self._cnt % self.reload_epochs == 0:
            self._first_load = True
        if replay_file is not None:
            self._is_replaying = True
            return self.replay_episode(replay_file, replay_config, actors)
        elif map_name not in self.map.name:
            self.load_world(map_name)
            self._first_load = False
        elif self._first_load:
            self.reload_world()
            self._first_load = False
        else:
            self.clear_traffic()

        for name, actor_config in actors.items():
            task = tasks.get(name, {})
            start = task.get("start", actor_config.get("spawn_point", None))
            if start is None:
                start = random.choice(self.spawn_points)
            else:
                start = CarlaActorConfig.parse_location(start, self.map)
                start = carla_util.carla_tranform(start)
                if actor_config.get("project_to_road", True):
                    start = self.map.get_waypoint(start.location).transform
                    start.location.z += 1.0

            if name not in self._actor_pool:
                actor_config["spawn_point"] = carla_util.localize_transform(start)
                self.create_actor(name, actor_config)

            self.teleport_actor(self.get_actor(name), start)
            if actor_config.get("autopilot", False):
                self.get_actor(name).set_autopilot(True, self.tm_port)
                self._autopilot_pool[name] = True

        self.tick()

        for name, sensor in self._sensor_pool.items():
            sensor.reset()
        self.tick()

        if self.spectator_transform is not None:
            spectator = self.world.get_spectator()
            spectator.set_transform(carla_util.carla_tranform(self.spectator_transform))
        if "weather" in scenario:
            weather = scenario["weather"]
            if isinstance(weather, list):
                weather = random.choice(weather)

            carla_weather = self.world.get_weather()
            for key, value in weather.items():
                if hasattr(carla_weather, key):
                    setattr(carla_weather, key, value)

            self.world.set_weather(carla_weather)
        if "traffic" in scenario:
            self.spawn_traffic(scenario["traffic"])

        for actor_id, task in tasks.items():
            init_speed = task.get("init_speed", 0)
            if init_speed > 0:
                actor = self.get_actor(actor_id)
                actor_forward = actor.get_transform().get_forward_vector()
                actor.set_target_velocity(actor_forward * init_speed)
        self.tick()

    def create_actor(self, name: str, actor_config: dict):
        bp = actor_config.get("blueprint", None)
        actor_type = actor_config.get("type", "vehicle")
        rolename = actor_config.get("rolename", name)
        spawn_point = actor_config.get("spawn_point", None)
        project_to_road = actor_config.get("project_to_road", True)
        actor_attributes = actor_config.get("attributes", {})
        sensors = actor_config.get("sensors", {})

        if bp is None:
            bp = CarlaActorConfig.default_blueprint(str(actor_type))

        blueprints = self.blueprint_library.filter(bp)
        if not blueprints:
            raise RuntimeError(f"Cannot find blueprint: {bp}")

        blueprint = random.choice(blueprints)
        blueprint.set_attribute("role_name", rolename)
        for key, value in actor_attributes.items():
            if blueprint.has_attribute(key):
                blueprint.set_attribute(key, str(value))
            else:
                logger.warning(f"Attribute {key} is not available for {blueprint.id}")

        if spawn_point is None:
            spawn_point = random.choice(self.spawn_points)
        else:
            spawn_point = CarlaActorConfig.parse_location(spawn_point, self.map)
            spawn_point = carla_util.carla_tranform(spawn_point)
            if project_to_road:
                spawn_point = self.map.get_waypoint(spawn_point.location).transform
        spawn_point.location += carla.Location(z=1.0)

        actor = self.world.try_spawn_actor(blueprint, spawn_point)
        if actor is None:
            raise RuntimeError(f"Failed to spawn actor: {name} at {spawn_point}")

        self.tick(flush_command=False)
        self._actor_pool[name] = actor

        for sensor_name, sensor_config in sensors.items():
            sensor_config["parent"] = name
            self.create_sensor(sensor_name, sensor_config)

    def create_sensor(self, name: str, sensor_config: dict):
        sensor_type = sensor_config.get("type", "rgb")
        bp = sensor_config.get("blueprint", None)
        parent = sensor_config.get("parent", None)
        spawn_point = sensor_config.get("spawn_point", None)
        pos_index = sensor_config.get("pos_index", 0)
        relative_pos = sensor_config.get("relative_pos", False)
        attachment_type = sensor_config.get("attachment_type", "rigid")
        sensor_attributes = sensor_config.get("attributes", {})
        sensor_callback = sensor_config.get("callback", None)

        # fmt: off
        if parent is None:
            parent = "global"
            parent_actor = None
        else:
            parent_actor = self.get_actor(parent)

        if sensor_type in ["bev", "birdeye", "bird_eye", "bird_eye_view"]:
            if parent_actor is None:
                raise RuntimeError("Parent actor is required for BEV sensor.")

            sensor = BirdViewProducer(
                adapter=self,
                parent=parent_actor,
                target_size=sensor_attributes.get("target_size", (512, 512)),
                render_lanes_on_junctions=sensor_attributes.get("render_lanes_on_junctions", False),
                pixels_per_meter=sensor_attributes.get("pixels_per_meter", 4),
                crop_type=sensor_attributes.get("crop_type", BirdViewCropType.FRONT_AND_REAR_AREA),
            )
        elif "speed" in sensor_type.lower():
            sensor = PseudoSensor(parent_actor, sensor_type="sensor.other.speedometer")
        else:
            if bp is None:
                bp = CarlaSensorConfig.default_blueprint(str(sensor_type))

            blueprints = self.blueprint_library.filter(bp)
            if not blueprints:
                raise RuntimeError(f"Cannot find blueprint: {bp}")

            blueprint = random.choice(blueprints)
            for key, value in sensor_attributes.items():
                if blueprint.has_attribute(key):
                    blueprint.set_attribute(key, str(value))
                else:
                    logger.warning(f"Attribute {key} is not available for {blueprint.id}")

            if spawn_point is None:
                if relative_pos and parent_actor is not None:
                    extent = parent_actor.bounding_box.extent
                    spawn_point = {}

                    for key in ["x", "y", "z"]:
                        attr = sensor_config.get(f"relative_{key}", (0, 0))
                        spawn_point[key] = attr[0] * (getattr(extent, key) + attr[1])
                    for key in ["pitch", "yaw", "roll"]:
                        spawn_point[key] = sensor_config.get(f"relative_{key}", 0)
                else:
                    spawn_point = CarlaSensorConfig._camera_transforms[pos_index]

            spawn_point = CarlaActorConfig.parse_location(spawn_point, self.map)
            spawn_point = carla_util.carla_tranform(spawn_point)

            attachment_type = attachment_type.title().replace("_", "")
            attachment_type = getattr(carla.AttachmentType, attachment_type, None)
            if attachment_type is None:
                raise ValueError(f"Unsupported attachment type: {sensor_config['attachment_type']}")

            sensor = self.world.try_spawn_actor(
                blueprint,
                spawn_point,
                attach_to=parent_actor,
                attachment_type=attachment_type,
            )
            if sensor is None:
                raise RuntimeError(f"Failed to spawn sensor: {name} at {spawn_point}")
            else:
                sensor_attributes.update(sensor.attributes)
                sensor_config["attributes"] = sensor_attributes

            self.tick(flush_command=False)
            if sensor_callback is not None:
                if callable(sensor_callback):
                    sensor.listen(sensor_callback)
                elif isinstance(sensor_callback, str):
                    module, fn_name = sensor_callback.rsplit(".", 1)
                    module = importlib.import_module(module)
                    sensor.listen(getattr(module, fn_name))

        # fmt: on
        self._sensor_pool[f"{parent}:{name}"] = wrap_sensor(sensor)

    @override(AdapterInterface)
    def tick(self, flush_command: bool = True):
        """Tick the simulator.

        Args:
            flush_command (bool, optional): Apply the command buffer before ticking. Defaults to True.

        Returns:
            int: The frame number after ticking.
        """

        if flush_command and self._command_buffer:
            self.client.apply_batch_sync(self._command_buffer)
            self._command_buffer.clear()

        if self.spectator_follow_actor is not None:
            try:
                actor = self.get_actor(self.spectator_follow_actor)
                transform = actor.get_transform()
                forward = transform.get_forward_vector()
                transform.location -= forward * 8
                transform.location += carla.Location(z=2.2)
                self.world.get_spectator().set_transform(transform)
            except RuntimeError:
                logger.warning(
                    f"Spectator follow actor {self.spectator_follow_actor} not found."
                )

        if self.sync_mode:
            frame = self.world.tick()
        else:
            snapshot = self.world.wait_for_tick()
            frame = snapshot.frame
        return frame

    def get_actor(self, identifier: str) -> carla.Actor:
        """Get the actor by identifier.

        Args:
            identifier (str): The identifier of the actor.

        Returns:
            carla.Actor: The actor object.

        Raises:
            RuntimeError: If the actor is not found.
        """
        for pool in (self._actor_pool, self._traffic_vehicles, self._traffic_walkers):
            actor = pool.get(identifier, None)
            if actor is not None:
                return actor
        raise RuntimeError(f"Actor {identifier} not found.")

    def get_sensor(self, identifier: str) -> CarlaSensor:
        """Get the sensor by identifier.

        Args:
            identifier (str): The identifier of the sensor. Note that the identifier for a sensor
            is the name of its parent actor and the sensor itself, separated by a colon.

        Returns:
            CarlaSensor: The wrapped sensor object.

        Raises:
            RuntimeError: If the sensor is not found.
        """

        sensor = self._sensor_pool.get(identifier, None)
        if sensor is None:
            raise RuntimeError(f"Sensor {identifier} not found.")
        return sensor

    def load_world(self, map_name: str) -> carla.World:
        self.reset()
        self._world = self.client.load_world(map_name)
        self.toggle_sync_mode(self.sync_mode, self.fixed_delta_seconds)
        return self._world

    def reload_world(self) -> carla.World:
        self.reset()
        self._world = self.client.reload_world()
        self.toggle_sync_mode(self.sync_mode, self.fixed_delta_seconds)
        return self._world

    @retry(RuntimeError, tries=5, delay=1, logger=logger)
    def connect(self, host: str, port: int) -> carla.Client:
        """Connects to the simulator.

        Args:
            host (str): The host to connect to.
            port (int): The port to connect to.

        Returns:
            The connected client.
        """

        client = carla.Client(host, port)
        client.set_timeout(5.0)
        server_version = client.get_server_version()
        logger.info("Connected to server, version: %s" % server_version)
        return client

    def teleport_actor(
        self, actor: carla.Actor, transform: carla.Transform, due_tick_cue: bool = False
    ):
        """Teleports an actor to a new location.

        Args:
            actor (carla.Actor): The actor to teleport.
            transform (carla.Transform): The new location.
            due_tick_cue (bool, optional): Whether to immediately take effect. Defaults to False.
        """

        ApplyWalkerControl = carla.command.ApplyWalkerControl
        ApplyVehicleControl = carla.command.ApplyVehicleControl
        ApplyTransform = carla.command.ApplyTransform
        SetSimulatePhysics = carla.command.SetSimulatePhysics
        buffer = self._command_buffer

        if isinstance(actor, carla.Walker):
            buffer.append(ApplyWalkerControl(actor, carla.WalkerControl()))
        elif isinstance(actor, carla.Vehicle):
            buffer.append(ApplyVehicleControl(actor, carla.VehicleControl()))

        buffer.append(SetSimulatePhysics(actor, False))
        buffer.append(ApplyTransform(actor, transform))
        buffer.append(SetSimulatePhysics(actor, True))

        if due_tick_cue:
            self.tick()

    def toggle_sync_mode(self, sync: bool, fixed_delta_seconds: float = None):
        """Toggles the sync mode of the simulator.

        Args:
            sync (bool): Whether to enable sync mode.
            fixed_delta_seconds (float, optional): The fixed delta seconds. Defaults to None.
        """

        self.sync_mode = sync
        settings = self.world.get_settings()
        settings.synchronous_mode = sync
        settings.fixed_delta_seconds = fixed_delta_seconds
        self.world.apply_settings(settings)
        self.traffic_manager.set_synchronous_mode(sync)

    def reset(self):
        """Resets the adapter state.

        Note: This will not reset the simulator state.
        """

        self._world = None
        self._map = None
        self._spawn_points = None
        self._blueprint_library = None
        self._hd_map = None
        self._actor_pool.clear()
        self._traffic_vehicles.clear()
        self._traffic_walkers.clear()
        self._sensor_pool.clear()
        self._global_callbacks.clear()
        self._command_buffer.clear()

    def parse_config(self):
        # connection
        self.start_server = self.config.get("start_server", False)
        self.offsreen = self.config.get("offscreen", False)
        if self.start_server:
            self.host = "localhost"
            self.port = carla_util.get_tcp_port()
        else:
            self.host = self.config.get("host", "localhost")
            self.port = self.config.get("port", 2000)
        self.timeout = self.config.get("timeout", 20.0)

        # sync
        self.sync_mode = self.config.get("sync_mode", True)
        self.fixed_delta_seconds = self.config.get("fixed_delta_seconds", 0.05)
        self.command_in_batch = self.config.get("command_in_batch", False)
        self.reload_epochs = self.config.get("reload_epochs", -1)

        # traffic_manager
        self.tm_port = self.config.get("tm_port", None)
        if self.tm_port is None:
            self.tm_port = carla_util.get_tcp_port()

        self.leading_distance = self.config.get("leading_distance", 5.0)
        self.hybrid_physics_mode = self.config.get("hybrid_physics_mode", False)
        self.hybrid_physics_radius = self.config.get("hybrid_physics_radius", 5.0)
        self.random_seed = self.config.get("random_seed", 0)

        # Spectator
        self.spectator_transform = self.config.get("spectator_location", None)
        self.spectator_follow_actor = self.config.get("spectator_follow_actor", None)

    def spawn_traffic(self, traffic_config: dict):
        # fmt: off
        num_vehicle = traffic_config.get("npc_vehicle_num", 0)
        num_walker = traffic_config.get("npc_walker_num", 0)
        running_percentage = traffic_config.get("running_walker_percentage", 0.0)
        crossing_percentage = traffic_config.get("crossing_walker_percentage", 0.0)
        spawn_near_agents = traffic_config.get("spawn_near_agents", True)
        radius = traffic_config.get("spawn_radius", 100.0)

        if spawn_near_agents:
            if "hero" in self._actor_pool:
                hero = self.get_actor("hero")
            else:
                hero = random.choice(list(self._actor_pool.values()))
        else:
            hero = None

        if num_vehicle:
            npc_vehicles = generate_vehicle_traffic(self, num_vehicle, hero, radius)
            self._traffic_vehicles.update(npc_vehicles)
        if num_walker:
            npc_walkers = generate_walker_traffic(self, num_walker, running_percentage, crossing_percentage, hero, radius)
            self._traffic_walkers.update(npc_walkers)

    def clear_traffic(self):
        batch = []
        vehicles = list(self._traffic_vehicles.values())
        walkers = list(self._traffic_walkers.values())
        for i in range(0, len(walkers), 2):
            walkers[i].stop()
        for vehicle in vehicles + walkers:
            batch.append(carla.command.DestroyActor(vehicle))

        self.client.apply_batch_sync(batch)
        self.tick()
        self._traffic_vehicles.clear()
        self._traffic_walkers.clear()

    def replay_episode(self, replay_file: str, replay_config: dict, actors: dict):
        replay = Replay(self._client, replay_file)
        if not self.map.name.endswith(replay.map_name):
            self.load_world(replay.map_name)
        else:
            self.reset()
        self.toggle_sync_mode(True, self.fixed_delta_seconds)

        # Get the id of the vehicle to follow
        follow_vehicle = replay_config.get("follow_vehicle", None)
        if isinstance(follow_vehicle, str):
            follow_vehicle = replay.get_actor_by_rolename(follow_vehicle)
            if follow_vehicle is None:
                raise ValueError(f"Vehicle with rolename {follow_vehicle} not found.")
            follow_id = follow_vehicle.id
        elif isinstance(follow_vehicle, int):
            follow_vehicle = replay.get_actor_by_id(follow_vehicle)
            if follow_vehicle is None:
                raise ValueError(f"Vehicle with id {follow_vehicle} not found.")
            follow_id = follow_vehicle.id
        else:
            follow_id = 0

        start_time = replay_config.get("start_time", 0)
        end_time = replay_config.get("end_time", 0)
        self._client.replay_file(replay_file, start_time, end_time, follow_id)

        # Tick till vehicle is spawned
        while True:
            self.tick()
            carla_actors = [self.world.get_actor(actor.id) for actor in replay.actors]
            if all(actor is not None for actor in carla_actors):
                break

        for actor in carla_actors:
            role_name = actor.attributes.get("role_name", f"actor_{actor.id}")
            if role_name in self._actor_pool:
                role_name = f"{role_name}_{actor.id}"
            self._actor_pool[role_name] = actor

        for name, actor_config in actors.items():
            if name == "random":
                if replay_config.get("vehicle_as_ego", True):
                    pool = list(
                        filter(
                            lambda x: isinstance(self._actor_pool[x], carla.Vehicle),
                            self._actor_pool.keys(),
                        )
                    )
                else:
                    pool = list(self._actor_pool.keys())
                name = random.choice(pool)

            sensors = actor_config.get("sensors", {})
            for sensor_name, sensor_config in sensors.items():
                sensor_config["parent"] = name
                self.create_sensor(sensor_name, sensor_config)
